﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Microsoft.AspNetCore.Mvc.IntegrationTests;

public class NullableReferenceTypeIntegrationTest
{
#nullable enable
    private class Person1
    {
        public string FirstName { get; set; } = default!;
    }
#nullable restore

    [Fact]
    public async Task BindProperty_WithNonNullableReferenceType_NoData_ValidationError()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo(),
            ParameterType = typeof(Person1)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var boundPerson = Assert.IsType<Person1>(modelBindingResult.Model);
        Assert.Null(boundPerson.FirstName);

        // ModelState
        Assert.False(modelState.IsValid);
        Assert.Collection(
            modelState.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("FirstName", kvp.Key);
                Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);

                // Not validating framework error message.
                Assert.Single(kvp.Value.Errors);
            });
    }

#nullable enable
    private class Person2
    {
        public string? FirstName { get; set; }
    }
#nullable restore

    [Fact]
    public async Task BindProperty_WithNullableReferenceType_NoData_NoValidationError()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo(),
            ParameterType = typeof(Person2)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var boundPerson = Assert.IsType<Person2>(modelBindingResult.Model);
        Assert.Null(boundPerson.FirstName);

        // ModelState
        Assert.True(modelState.IsValid);
    }

#nullable enable
    private class Person3
    {
        [Required(ErrorMessage = "Test")]
        public string FirstName { get; set; } = default!;
    }
#nullable restore

    [Fact]
    public async Task BindProperty_WithNonNullableReferenceType_NoData_ValidationError_CustomMessage()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "Parameter1",
            BindingInfo = new BindingInfo(),
            ParameterType = typeof(Person3)
        };

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);

        // Assert

        // ModelBindingResult
        Assert.True(modelBindingResult.IsModelSet);

        // Model
        var boundPerson = Assert.IsType<Person3>(modelBindingResult.Model);
        Assert.Null(boundPerson.FirstName);

        // ModelState
        Assert.False(modelState.IsValid);
        Assert.Collection(
            modelState.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("FirstName", kvp.Key);
                Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);

                var error = Assert.Single(kvp.Value.Errors);
                Assert.Equal("Test", error.ErrorMessage);
            });
    }

#nullable enable
    private void NonNullableParameter(string param1)
    {
    }
#nullable restore

    [Fact]
    public async Task BindParameter_WithNonNullableReferenceType_NoData_ValidationError()
    {
        // Arrange
        var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
        var parameter = new ParameterDescriptor()
        {
            Name = "param1",
            BindingInfo = new BindingInfo(),
            ParameterType = typeof(string)
        };

        var method = GetType().GetMethod(nameof(NonNullableParameter), BindingFlags.NonPublic | BindingFlags.Instance);
        var parameterInfo = method.GetParameters().Single();
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
        var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);

        var testContext = ModelBindingTestHelper.GetTestContext();
        var modelState = testContext.ModelState;

        // Act
        var modelBindingResult = await parameterBinder.BindModelAsync(
            parameter,
            testContext,
            modelMetadataProvider,
            modelMetadata);

        // Assert

        // ModelBindingResult
        Assert.False(modelBindingResult.IsModelSet);

        // Model
        Assert.Null(modelBindingResult.Model);

        // ModelState
        Assert.False(modelState.IsValid);
        Assert.Collection(
            modelState.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("param1", kvp.Key);
                Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);

                // Not validating framework error message.
                Assert.Single(kvp.Value.Errors);
            });
    }
}
